package ph.com.gs3.formalistics.services.synchronizers;

import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import ph.com.gs3.formalistics.global.constants.LoggingType;
import ph.com.gs3.formalistics.global.constants.UserUpdateOptions;
import ph.com.gs3.formalistics.global.utilities.logging.FLLogger;
import ph.com.gs3.formalistics.model.RemoteDataAccessObject.RDAOCommunicationException;
import ph.com.gs3.formalistics.model.facades.DocumentsDataReader;
import ph.com.gs3.formalistics.model.facades.DocumentsDataWriter;
import ph.com.gs3.formalistics.model.facades.FormsDataReader;
import ph.com.gs3.formalistics.model.ldao.DocumentsLDAO;
import ph.com.gs3.formalistics.model.ldao.FormsLDAO;
import ph.com.gs3.formalistics.model.rdao.DocumentsRDAO;
import ph.com.gs3.formalistics.model.vo.application.APIResponse;
import ph.com.gs3.formalistics.model.vo.application.APIResponse.APIInvalidResponseException;
import ph.com.gs3.formalistics.model.vo.business.User;
import ph.com.gs3.formalistics.model.vo.business.document.Document;
import ph.com.gs3.formalistics.model.vo.business.form.Form;
import ph.com.gs3.formalistics.model.vo.parsers.json.DocumentsJSONParser;
import ph.com.gs3.formalistics.model.vo.parsers.json.UserJSONParser;
import ph.com.gs3.formalistics.services.synchronizers.exceptions.SynchronizationFailedException;
import ph.com.gs3.formalistics.services.synchronizers.exceptions.SynchronizationPrematureException;
import android.content.Context;

public class DocumentsSynchronizer extends AbstractSynchronizer<Void> {

	public static final String TAG = DocumentsSynchronizer.class.getSimpleName();
	public static LoggingType LOGGING_TYPE;

	protected User user;

	protected UsersSynchronizer usersSynchronizer;

	protected FormsDataReader formsDataReader;
	protected DocumentsDataReader documentsDataReader;
	protected DocumentsDataWriter documentsDataWriter;

	protected DocumentsRDAO documentsRDAO;
	protected DocumentsLDAO documentsLDAO;
	protected FormsLDAO formsLDAO;

	protected DocumentsSynchronizerEventListener eventListener;

	public DocumentsSynchronizer(Context context, User user,
	        DocumentsSynchronizerEventListener eventListener) {
		super(TAG, LOGGING_TYPE == null ? LoggingType.DISABLED : LOGGING_TYPE);
		this.user = user;

		String server = user.getCompany().getServer();

		usersSynchronizer = new UsersSynchronizer(context, user);

		formsDataReader = new FormsDataReader(context, user);
		documentsDataReader = new DocumentsDataReader(context);
		documentsDataWriter = new DocumentsDataWriter(context, user);

		documentsRDAO = new DocumentsRDAO(server);
		documentsLDAO = new DocumentsLDAO(context);
		formsLDAO = new FormsLDAO(context);

		this.eventListener = eventListener;

	}

	public Void synchronize() throws SynchronizationPrematureException {

		List<Form> forms = formsDataReader.getCompanyForms();

		List<SynchronizationFailedException> syncFailures = new ArrayList<>();

		for (Form form : forms) {
			log("Synchronizing " + form.toString() + "'s documents");
			try {
				synchronizeFormDocuments(form);
				if (eventListener != null) {
					eventListener.onNewDocumentsDownloaded();
				}
			} catch (SynchronizationFailedException e) {
				syncFailures.add(e);
			}
			log("Done");
		}

		if (syncFailures.size() > 0) {
			throw new SynchronizationPrematureException(syncFailures);
		}

		return null;

	}

	/**
	 * Extracts user (author and processor) data from the document update
	 * (documentUpdateJSON) and saves any updates to the database.
	 * 
	 * @param documentUpdateJSON
	 * @throws SynchronizationFailedException
	 */
	protected Document synchronizeUsersFromDocumentUpdate(Document document,
	        JSONObject documentUpdateJSON) throws SynchronizationFailedException {

		EnumSet<UserUpdateOptions> userUpdateOptions = EnumSet
		        .of(UserUpdateOptions.UPDATE_EXCEPT_IS_ACTIVE,
		                UserUpdateOptions.UPDATE_EXCEPT_PASSWORD);

		// Retrieve the user from the JSON and update the database
		try {
			User author = UserJSONParser.createAuthorFromDocumentJSON(documentUpdateJSON);
			author.setCompany(user.getCompany());
			author = usersSynchronizer.updateUser(author, userUpdateOptions);
			document.setAuthorId(author.getId());
		} catch (JSONException e) {
			throw new SynchronizationFailedException(
			        "The server gave an invalid response about a document's author/requestor data.",
			        e);
		}

		// Retrieve the processor from the JSON and update the database
		try {
			User processor = UserJSONParser.createProcessorFromDocumentJSON(documentUpdateJSON);
			if (processor != null) {
				processor.setCompany(user.getCompany());
				processor = usersSynchronizer.updateUser(processor, userUpdateOptions);
				document.setProcessorId(processor.getId());
			}
		} catch (JSONException e) {
			throw new SynchronizationFailedException(
			        "The server gave an invalid response about a document's processor data.", e);
		}

		return document;

	}

	public void synchronizeFormDocuments(Form form) throws SynchronizationFailedException {

		String documentsLastUpdateDate;
		int userDocumentCount = documentsDataReader.getUserDocumentCount(user.getId());

		if (userDocumentCount > 0) {
			documentsLastUpdateDate = form.getDocumentsLastUpdateDate();
			log("Getting documents updated later than " + documentsLastUpdateDate);
		} else {
			documentsLastUpdateDate = null;
			log("Getting all documents");
		}

		APIResponse response;
		try {
			// TODO: add a limit here
			response = documentsRDAO.getFormDocumentUpdates(form.getWebId(),
			        documentsLastUpdateDate);

		} catch (APIInvalidResponseException e) {
			throw new SynchronizationFailedException(
			        "The server gave an invalid response while getting form document updates for form web id = "
			                + form.getWebId(), e);
		} catch (RDAOCommunicationException e) {
			throw new SynchronizationFailedException(e);
		}

		if (response.isOperationSuccessful()) {

			// Throws JSONException
			JSONArray documentsJSONArray;
			try {
				documentsJSONArray = new JSONArray(response.getResults());
			} catch (JSONException e) {
				throw new SynchronizationFailedException(
				        "The server response is not a valid JSON array.", e);
			}

			int documentCount = documentsJSONArray.length();

			for (int i = 0; i < documentCount; i++) {

				// Throws JSONException
				JSONObject documentJSON;
				Document document;

				try {
					documentJSON = new JSONObject(documentsJSONArray.getString(i));
					// Extract document
					document = DocumentsJSONParser.createFromJSON(documentJSON,
					        form.getActiveFields());
				} catch (JSONException e) {
					throw new SynchronizationFailedException(
					        "One or more of the document JSON objects is invalid", e);
				}

				// Get the users (author and processor)
				document = synchronizeUsersFromDocumentUpdate(document, documentJSON);

				// Build the document using extracted data and current user data
				document.setFormId(form.getId());

				// Save the document
				document = documentsDataWriter.updateOrSaveDocument(document, form);

				log("Updated document: " + document.toString());
			}

			// Update the documents' last update date of the current form
			try {
				formsLDAO.open();
				formsLDAO.updateDocumentsLastUpdateDate(form.getId(), response.getServerDate());
			} finally {
				formsLDAO.close();
			}
		} else {
			FLLogger.w(TAG, "Failed to get document updates: " + response.getErrorMessage());
		}

	}

	public static interface DocumentsSynchronizerEventListener {
		public void onNewDocumentsDownloaded();

		public void onAllFormsDownloaded();
	}
}
